﻿namespace ChoBulkFileProcessor
{
    #region NameSpaces

    using System;
    using System.Collections.Generic;
    using System.Text;
    using System.IO;
    using System.Text.RegularExpressions;
    using System.Windows;
    using Cinchoo.Core.IO;
    using Cinchoo.Core;
    using System.Threading;

    #endregion NameSpaces

    public enum ChoFileProcessStatus { INFO, SUCCESS, FAILED }

    public class ChoFileProcessEventArgs : EventArgs
    {
        #region Instance Data Members (Public)

        public ChoFileProcessStatus CleanupStatus
        {
            get;
            private set;
        }

        public string Message
        {
            get;
            private set;
        }

        public object Tag
        {
            get;
            private set;
        }

        #endregion Instance Data Members (Public)

        #region Constructors

        public ChoFileProcessEventArgs(ChoFileProcessStatus cleanupStatus, string message, object tag = null)
        {
            CleanupStatus = cleanupStatus;
            Message = message;
            Tag = tag;
        }

        #endregion Constructors
    }

    public class ChoFileFolderProcessor : IDisposable
    {
        #region Shared Data Members (Private)

        private static readonly ChoAppSettings _appSettings = new ChoAppSettings();

        #endregion Shared Data Members (Private)

        #region EventHandlers

        public event EventHandler<ChoFileProcessEventArgs> Status;
        public event EventHandler<ChoFileProcessEventArgs> AppStatus;

        #endregion EventHandlers

        #region Instance Data Members (Private)

        private string _moveOrCopyDirectory;
        private ChoPlugInsDefManager _plugInsDefManager;

        private int _collisionFileCounter = 0;
        private int _collisionDirCounter = 0;

        private readonly List<string> _fileList = new List<string>();

        private ChoPlugInsRuntime _prBulkFileOps = null;
        private ChoPlugInsRuntime _prUndoFileOps = null;
        private ChoPlugInsRuntime _prMatchFileOps = null;
        private ChoPlugInsRuntime _prProcessFileOps = null;

        private Regex _fileFindMatchingRegex;
        private bool _cancelRequested = false;

        #endregion Instance Data Members (Private)

        #region Constructors

        public ChoFileFolderProcessor(string settingsFilePath = null)
        {
            if (settingsFilePath.IsNullOrWhiteSpace())
            {
                if (File.Exists(settingsFilePath))
                {
                    string settingsText = File.ReadAllText(settingsFilePath);
                    _appSettings.LoadXml(settingsText);
                }
            }

            _plugInsDefManager = new ChoPlugInsDefManager(_appSettings.GetPlugInsDefFile());
        }

        #endregion Constructors

        #region Instance Members (Public)

        public void Process(string folder)
        {
            if (folder.IsNullOrWhiteSpace()) return;

            folder = folder.Trim();
            folder = ChoPath.GetFullPath(folder);

            _moveOrCopyDirectory = _appSettings.MoveOrCopyToDirectory.NTrim();

            _fileList.Clear();

            if (!_moveOrCopyDirectory.IsNullOrWhiteSpace())
            {
                //if (String.Compare(folder, _moveOrCopyDirectory, true) == 0)
                //    return;

                _moveOrCopyDirectory = ChoPath.GetFullPath(_moveOrCopyDirectory);

                //Check if the move to is not same as folder
                if (folder.StartsWith(_moveOrCopyDirectory)
                    || _moveOrCopyDirectory.StartsWith(folder))
                {
                    Status.Raise(this, new ChoFileProcessEventArgs(ChoFileProcessStatus.INFO, "SKIP: '{1}' folder can't be processed successfully due to conflict with MoveOrCopyDirectory.".FormatString(folder)));
                    return;
                }
                
                Directory.CreateDirectory(_moveOrCopyDirectory);
            }
            _fileFindMatchingRegex = _appSettings.GetFileFindMatchingRegex();

            if (Directory.Exists(folder))
            {
                if (!BulkFileOps(folder, ChoAppSettings.BEGIN_FILE_OPS_TAG))
                    return;

                if (_cancelRequested)
                    return;

                try
                {
                    ProcessFilesFromDirectory(folder, folder);
                    BulkFileOps(folder, ChoAppSettings.SUCCESS_FILE_OPS_TAG);
                }
                catch
                {
                    BulkFileOps(folder, ChoAppSettings.FAILED_FILE_OPS_TAG);
                    throw;
                }
            }

            AppStatus.Raise(this, new ChoFileProcessEventArgs(ChoFileProcessStatus.SUCCESS, "'{0}' folder processed successfully.".FormatString(folder)));
        }

        public void CancelProcessing()
        {
            ChoPlugInsRuntime prBulkFileOps = _prBulkFileOps;
            if (prBulkFileOps != null)
                prBulkFileOps.Cancel();

            ChoPlugInsRuntime prMatchFileOps = _prMatchFileOps;
            if (prMatchFileOps != null)
                prMatchFileOps.Cancel();

            ChoPlugInsRuntime prProcessFileOps = _prProcessFileOps;
            if (prProcessFileOps != null)
                prProcessFileOps.Cancel();

            _cancelRequested = true;
        }

        public void CancelUndo()
        {
            ChoPlugInsRuntime prUndo = _prUndoFileOps;
            if (prUndo == null) return;

            prUndo.Cancel();
        }

        public string Undo(string undoFilePath)
        {
            StringBuilder output = new StringBuilder();
            bool canContinue = true;

            if (_plugInsDefManager != null)
            {
                foreach (ChoUndoFileProcessState fileEntry in ChoObject.XmlDeserializeFromFile<List<ChoUndoFileProcessState>>(undoFilePath))
                {
                    canContinue = DoUndo(canContinue, fileEntry);
                    if (!canContinue)
                        break;
                }

                if (canContinue)
                    canContinue = DoUndo(canContinue, null);
            }

            if (canContinue)
            {
                try
                {
                    var x = ChoUndoFileProcessManager.Default.Value;
                    x.Undo(undoFilePath);
                }
                catch (Exception ex)
                {
                    output.AppendLine(ex.Message);
                }
            }

            return output.ToString();
        }

        private bool BulkFileOps(string folderPath, string plugInTag)
        {
            bool canContinue = true;
            string plugInTagCaption = plugInTag.ToUpper();

            if (plugInTag == ChoAppSettings.BEGIN_FILE_OPS_TAG)
                plugInTagCaption = "BEGIN_OPS";
            else if (plugInTag == ChoAppSettings.SUCCESS_FILE_OPS_TAG)
                plugInTagCaption = "SUCCESS_OPS";
            else if (plugInTag == ChoAppSettings.FAILED_FILE_OPS_TAG)
                plugInTagCaption = "FAILED_OPS";

            if (plugInTag == ChoAppSettings.BEGIN_FILE_OPS_TAG)
                Status.Raise(this, new ChoFileProcessEventArgs(ChoFileProcessStatus.INFO, "{0}: {1}".FormatString(plugInTagCaption, "Starting file operations...")));

            try
            {
                //Plug-in must return false to cancel the chain
                _prBulkFileOps = new ChoPlugInsRuntime(_plugInsDefManager.GetPlugInBuilders(plugInTag));
                _prBulkFileOps.BeforePlugInRun += ((o, e) =>
                {
                    e.Value = @"{0} ""{1}""".FormatString(_appSettings.PreviewMode, folderPath);
                });

                _prBulkFileOps.AfterPlugInRun += ((o, e) =>
                {
                    if (e.Value == null) return;

                    string[] o1 = e.Value.ToString().LastLine().SplitNTrim();
                    bool cancel = false;
                    Boolean.TryParse(o1.GetNValue(0), out cancel);

                    if (!o1.GetNValue(1).IsNullOrWhiteSpace())
                        Status.Raise(this, new ChoFileProcessEventArgs(ChoFileProcessStatus.INFO, "{0}: {1}".FormatString(plugInTagCaption, o1.GetNValue(1))));

                    e.Cancel = cancel;
                    e.Value = cancel;
                });


                object retVal = _prBulkFileOps.Run(null);
                if (retVal == null)
                    canContinue = true;
                else
                    Boolean.TryParse(retVal.ToNString(), out canContinue);
            }
            finally
            {
                _prBulkFileOps = null;
            }

            if (plugInTag == ChoAppSettings.SUCCESS_FILE_OPS_TAG)
                Status.Raise(this, new ChoFileProcessEventArgs(ChoFileProcessStatus.INFO, "{0}: {1}".FormatString(plugInTagCaption, "File operations are sucessfully completed.")));
            else if (plugInTag == ChoAppSettings.FAILED_FILE_OPS_TAG)
                Status.Raise(this, new ChoFileProcessEventArgs(ChoFileProcessStatus.INFO, "{0}: {1}".FormatString(plugInTagCaption, "File operations are failed to complete.")));
            return canContinue;
        }

        private bool DoUndo(bool canContinue, ChoUndoFileProcessState fileEntry)
        {
            try
            {
                //Plug-in must return false to cancel the chain
                _prUndoFileOps = new ChoPlugInsRuntime(_plugInsDefManager.GetPlugInBuilders(ChoAppSettings.UNDO_FILE_OPS_TAG));
                _prUndoFileOps.BeforePlugInRun += ((o, e) =>
                {
                    if (fileEntry != null)
                        e.Value = @"{0} {1} ""{2}"" ""{3}""".FormatString(_appSettings.PreviewMode, fileEntry.IsFolder, fileEntry.SourceFilePath, fileEntry.DestFilePath);
                    else
                        e.Value = @"{0}".FormatString(_appSettings.PreviewMode);
                });

                _prUndoFileOps.AfterPlugInRun += ((o, e) =>
                {
                    if (e.Value == null) return;

                    string[] o1 = e.Value.ToString().LastLine().SplitNTrim();
                    bool cancel = false;
                    Boolean.TryParse(o1.GetNValue(0), out cancel);

                    if (!o1.GetNValue(1).IsNullOrWhiteSpace())
                        Status.Raise(this, new ChoFileProcessEventArgs(ChoFileProcessStatus.INFO, o1.GetNValue(1)));

                    e.Cancel = cancel;
                    e.Value = cancel;
                });


                object retVal = _prUndoFileOps.Run(null);
                if (retVal == null)
                    canContinue = true;
                else
                    Boolean.TryParse(retVal.ToNString(), out canContinue);
            }
            finally
            {
                _prUndoFileOps = null;
            }
            return canContinue;
        }

        #endregion Instance Members (Public)

        #region Instance Members (Private)

        private void ProcessFile(string filePath, string rootDir)
        {
            AppStatus.Raise(this, new ChoFileProcessEventArgs(ChoFileProcessStatus.INFO, filePath));
            string newFilePath = null;

            try
            {
                string fileName = Path.GetFileName(filePath);
                if (IsMatch(fileName, filePath, false))
                {
                    newFilePath = ProcessFile(fileName, filePath, false);

                    if (newFilePath.IsNullOrWhiteSpace() || filePath == newFilePath)
                    {
                        ChoUndoFileProcessManager.Default.Value.AddUndoEntry(filePath, null, false, false);
                        return;
                    }

                    //if (!newFilePath.IsNullOrWhiteSpace())
                    //    newFileName = Path.GetFileName(newFilePath);
                    newFilePath = ChoPath.CleanPath(newFilePath);
                    if (Path.GetDirectoryName(filePath) == Path.GetDirectoryName(newFilePath))
                    {
                        //newFilePath = Path.Combine(Path.GetDirectoryName(filePath), newFilePath);
                        if (!_moveOrCopyDirectory.IsNullOrWhiteSpace())
                            newFilePath = newFilePath.Replace(rootDir, _moveOrCopyDirectory);
                    }

                    if (_fileList.Contains(newFilePath))
                    {
                        if (_appSettings.ResolveCollision)
                        {
                            newFilePath = Path.Combine(Path.GetDirectoryName(newFilePath), "{0}_{1}{2}".FormatString(Path.GetFileNameWithoutExtension(newFilePath),
                                ++_collisionFileCounter, Path.GetExtension(newFilePath)));
                            Status.Raise(this, new ChoFileProcessEventArgs(ChoFileProcessStatus.INFO, "COLL RESOLV [FILE]: {0} -> {1}".FormatString(filePath, newFilePath)));
                        }
                        else
                        {
                            Status.Raise(this, new ChoFileProcessEventArgs(ChoFileProcessStatus.INFO, "COLL SKIP [FILE]: {0} -> {1}".FormatString(filePath, newFilePath)));
                            return;
                        }
                    }

                    if (!_appSettings.PreviewMode)
                        ChoDirectory.CreateDirectoryFromFilePath(newFilePath);

                    if (_moveOrCopyDirectory.IsNullOrWhiteSpace())
                    {
                        if (!_appSettings.PreviewMode)
                            ChoFile.Move(filePath, newFilePath, _appSettings.Overwrite);
                        ChoUndoFileProcessManager.Default.Value.AddUndoEntry(filePath, newFilePath, false, false);
                    }
                    else
                    {
                        if (_appSettings.DoCopy)
                        {
                            if (!_appSettings.PreviewMode)
                                File.Copy(filePath, newFilePath, _appSettings.Overwrite);
                            ChoUndoFileProcessManager.Default.Value.AddUndoEntry(filePath, newFilePath, false, true);
                        }
                        else
                        {
                            if (!_appSettings.PreviewMode)
                                ChoFile.Move(filePath, newFilePath, _appSettings.Overwrite);
                            ChoUndoFileProcessManager.Default.Value.AddUndoEntry(filePath, newFilePath, false, false);
                        }
                    }

                    _fileList.Add(newFilePath);
                    Status.Raise(this, new ChoFileProcessEventArgs(ChoFileProcessStatus.SUCCESS, "SUCCESS [FILE]: {0} -> {1}".FormatString(filePath, newFilePath)));
                }
            }
            catch (Exception ex)
            {
                Status.Raise(this, new ChoFileProcessEventArgs(ChoFileProcessStatus.FAILED, "FAILED [FILE]: {0} - {1} [ERR_MSG: {2}]".FormatString(filePath, newFilePath, ex.Message)));
            }
        }

        private void ProcessFilesFromDirectory(string path, string rootDir)
        {
            AppStatus.Raise(this, new ChoFileProcessEventArgs(ChoFileProcessStatus.INFO, path));
            try
            {
                if (_appSettings.IsFileProcessEnabled)
                {
                    foreach (string filePath in Directory.GetFiles(path))
                    {
                        if (_cancelRequested)
                            return;

                        ProcessFile(filePath, rootDir);
                    }
                }

                if (_appSettings.IncludeSubDir)
                {
                    foreach (string subDirectory in Directory.GetDirectories(path))
                        ProcessFilesFromDirectory(subDirectory, rootDir);
                }

                string directoryName = Path.GetFileName(path);

                if (!_appSettings.IsFolderProcessEnabled)
                    return;

                if (IsMatch(directoryName, path, true))
                {
                    string newDirectoryPath = ProcessFile(directoryName, path, true);
                    if (newDirectoryPath.IsNullOrWhiteSpace())
                    {
                        ChoUndoFileProcessManager.Default.Value.AddUndoEntry(path, null, true, false);
                        return;
                    }

                    string newPath = newDirectoryPath;
                    if (!Path.IsPathRooted(newDirectoryPath))
                        newPath = Path.Combine(Path.GetDirectoryName(path), newDirectoryPath);

                    if (!_moveOrCopyDirectory.IsNullOrWhiteSpace())
                        newPath = newPath.Replace(rootDir, _moveOrCopyDirectory);

                    if (path == rootDir)
                    {
                        if (!_moveOrCopyDirectory.IsNullOrWhiteSpace())
                            ChoUndoFileProcessManager.Default.Value.AddUndoEntry(path, newPath, true, !_moveOrCopyDirectory.IsNullOrWhiteSpace());
                        else
                            ChoUndoFileProcessManager.Default.Value.AddUndoEntry(path, null, true, false);

                        return;
                    }

                    if (_fileList.Contains(newPath))
                    {
                        if (_appSettings.ResolveCollision)
                        {
                            newPath = "{0}_{2}".FormatString(newPath, ++_collisionDirCounter);
                            Status.Raise(this, new ChoFileProcessEventArgs(ChoFileProcessStatus.INFO, "COLL REOLV [DIR]: {0} -> {1}".FormatString(path, newPath)));
                        }
                        else
                        {
                            Status.Raise(this, new ChoFileProcessEventArgs(ChoFileProcessStatus.INFO, "COLL SKIP [DIR]: {0} -> {1}".FormatString(path, newPath)));
                            return;
                        }
                    }

                    if (!_appSettings.PreviewMode)
                    {
                        if (path != newDirectoryPath)
                            Directory.Move(path, newPath);
                        else if (_appSettings.CreateEmptyDir)
                            Directory.CreateDirectory(newPath);

                        if (path != newPath)
                        {
                            ChoUndoFileProcessManager.Default.Value.AddUndoEntry(path, newPath, true, !_moveOrCopyDirectory.IsNullOrWhiteSpace());
                            Status.Raise(this, new ChoFileProcessEventArgs(ChoFileProcessStatus.SUCCESS, "SUCCESS [DIR]: {0} -> {1}".FormatString(path, newPath)));
                        }
                        else
                            ChoUndoFileProcessManager.Default.Value.AddUndoEntry(path, null, true, false);
                    }

                    _fileList.Add(newPath);
                }
            }
            catch (Exception ex)
            {
                Status.Raise(this, new ChoFileProcessEventArgs(ChoFileProcessStatus.FAILED, "FAILED [DIR]: {0} - {1}".FormatString(path, ex.Message)));
            }
        }

        private bool IsMatch(string fileName, string filePath, bool isFolder)
        {
            if (_fileFindMatchingRegex != null)
            {
                if (!_fileFindMatchingRegex.IsMatch(filePath))
                    return false;
            }

            bool isMatchFound = true;
            if (_plugInsDefManager != null)
            {
                try
                {
                    //Plug-in must return true/NULL to continue the chain, otherwise false
                    _prMatchFileOps = new ChoPlugInsRuntime(_plugInsDefManager.GetPlugInBuilders(ChoAppSettings.MATCH_FILE_OPS_TAG));
                    _prMatchFileOps.BeforePlugInRun += ((o, e) =>
                        {
                            e.Value = @"{0} {1} ""{2}"" ""{3}""".FormatString(_appSettings.PreviewMode, isFolder, fileName, filePath);
                        });

                    _prMatchFileOps.AfterPlugInRun += ((o, e) =>
                        {
                            if (e.Value == null) return;

                            string[] o1 = e.Value.ToString().LastLine().SplitNTrim();
                            bool isMatch = false;
                            Boolean.TryParse(o1.GetNValue(0), out isMatch);

                            if (!o1.GetNValue(1).IsNullOrWhiteSpace())
                            {
                                if (!isFolder)
                                    Status.Raise(this, new ChoFileProcessEventArgs(ChoFileProcessStatus.INFO, "FILE MATCH: {0}".FormatString(o1.GetNValue(1))));
                                else
                                    Status.Raise(this, new ChoFileProcessEventArgs(ChoFileProcessStatus.INFO, "DIR MATCH: {0}".FormatString(o1.GetNValue(1))));
                            }

                            e.Value = isMatch;
                            e.Cancel = !isMatch;
                        });


                    object retVal = _prMatchFileOps.Run(null);
                    if (retVal == null)
                        isMatchFound = true;
                    else
                    {
                        bool isMatch = false;
                        Boolean.TryParse(retVal.ToNString(), out isMatch);
                        isMatchFound = isMatch;
                    }
                }
                finally
                {
                    _prMatchFileOps = null;
                }
            }

            if (isMatchFound)
            {
                if (!isFolder)
                    Status.Raise(this, new ChoFileProcessEventArgs(ChoFileProcessStatus.INFO, "FILE MATCH: {0}".FormatString(filePath)));
                else
                    Status.Raise(this, new ChoFileProcessEventArgs(ChoFileProcessStatus.INFO, "DIR MATCH: {0}".FormatString(filePath)));
            }
            return isMatchFound;
        }

        private string ProcessFile(string fileName, string filePath, bool isFolder)
        {
            string newFileName = fileName;
            if (_appSettings.UseWildcard)
            {
                if (!_appSettings.FileFindMatchingExpr.IsNullOrWhiteSpace() && !_appSettings.FileReplaceMatchingExpr.IsNullOrWhiteSpace())
                    newFileName = fileName.SearchNReplace(_appSettings.FileFindMatchingExpr, _appSettings.FileReplaceMatchingExpr);
            }
            else if (_fileFindMatchingRegex != null && !_appSettings.FileReplaceMatchingExpr.IsNullOrWhiteSpace())
                newFileName = _fileFindMatchingRegex.Replace(fileName, _appSettings.FileReplaceMatchingExpr);

            newFileName = newFileName.ExpandProperties(newFileName);
            if (!_appSettings.FileTransText.IsNullOrEmpty())
            {
                foreach (string t in _appSettings.FileTransText.SplitNTrim(';'))
                {
                    if (t.StartsWith("String."))
                        newFileName = "~~((string)@this).{0}~~".FormatString(t.Replace("String.", String.Empty)).ExpandProperties(newFileName);
                    else
                        newFileName = t.ExpandProperties(newFileName);
                }
            }

            filePath = Path.Combine(Path.GetDirectoryName(filePath), ChoPath.CleanFileName(newFileName));

            if (_plugInsDefManager != null)
            {
                try
                {
                    //Plug-in must return new file name, otherwise chain will stop
                    _prProcessFileOps = new ChoPlugInsRuntime(_plugInsDefManager.GetPlugInBuilders(ChoAppSettings.PROCESS_FILE_OPS_TAG));
                    _prProcessFileOps.BeforePlugInRun += ((o, e) =>
                    {
                        e.Value = @"{0} {1} ""{2}"" ""{3}""".FormatString(_appSettings.PreviewMode, isFolder, newFileName, filePath);
                    });

                    _prProcessFileOps.AfterPlugInRun += ((o, e) =>
                    {
                        if (e.Value == null) return;

                        string output = e.Value.ToString().LastLine();
                        if (!output.IsNullOrWhiteSpace())
                        {
                            string[] o1 = output.SplitNTrim();

                            if (!e.IsLastPlugIn)
                            {
                                ParseOutput(o1.GetNValue(0), ref newFileName, ref filePath);
                                e.Value = @"{0} {1} ""{2}"" ""{3}""".FormatString(_appSettings.PreviewMode, isFolder, newFileName, filePath);
                            }
                            else
                                e.Value = o1.GetNValue(0);

                            if (!o1.GetNValue(1).IsNullOrWhiteSpace())
                            {
                                if (!isFolder)
                                    Status.Raise(this, new ChoFileProcessEventArgs(ChoFileProcessStatus.INFO, "FILE PROCESS: {0}".FormatString(o1.GetNValue(1))));
                                else
                                    Status.Raise(this, new ChoFileProcessEventArgs(ChoFileProcessStatus.INFO, "DIR PROCESS: {0}".FormatString(o1.GetNValue(1))));
                            }
                        }
                        else
                            e.Cancel = true;
                    });

                    string output1 = _prProcessFileOps.Run(null) as string;
                    output1 = output1.LastLine();
                    ParseOutput(output1.SplitNTrim().GetNValue(0), ref newFileName, ref filePath);

                    return filePath;
                }
                finally
                {
                    _prProcessFileOps = null;
                }
            }

            return filePath;
        }

        private void ParseOutput(string output, ref string newFileName, ref string filePath)
        {
            if (output.IsNullOrWhiteSpace()) return;
            if (Path.IsPathRooted(output))
            {
                newFileName = ChoPath.CleanFileName(Path.GetFileName(output));
                filePath = output;
            }
            else
            {
                newFileName = ChoPath.CleanFileName(output);
                filePath = Path.Combine(filePath, newFileName);
            }
        }

        #endregion Instance Members (Private)

        #region IDisposable Members

        public void Dispose()
        {
        }

        #endregion
    }
}
